package com.dbflow5.query;

import com.dbflow5.StringUtils;
import com.dbflow5.database.DatabaseWrapper;
import com.dbflow5.database.FlowCursor;
import com.dbflow5.query.property.IProperty;
import com.dbflow5.sql.QueryCloneable;
import com.dbflow5.structure.ChangeAction;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * Description: Defines the SQL WHERE statement of the query.
 */
public class Where<T> extends BaseModelQueriable<T> implements ModelQueriable<T>, Transformable<T>, QueryCloneable<Where<T>> {
    private static final long VALUE_UNSET = -1L;
    /**
     * Helps to build the where statement easily
     */
    private final OperatorGroup operatorGroup = OperatorGroup.nonGroupingClause();

    private final List<NameAlias> groupByList = new ArrayList<>();

    private final List<OrderBy> orderByList = new ArrayList<>();

    /**
     * The SQL HAVING statement
     */
    private final OperatorGroup havingGroup = OperatorGroup.nonGroupingClause();

    private long limit = VALUE_UNSET;
    private long offset = VALUE_UNSET;

    public final WhereBase<T> whereBase;

    public Where(WhereBase<T> whereBase, SQLOperator... conditions){
        super(whereBase.table());
        this.whereBase = whereBase;
        operatorGroup.andAll(Arrays.asList(conditions));
    }

    @Override
    public ChangeAction primaryAction() {
        return whereBase.primaryAction();
    }

    @Override
    public Where<T> cloneSelf() {
        Where<T> where = new Where<>(whereBase.cloneSelf(), operatorGroup.conditions().toArray(new SQLOperator[]{}));
        where.groupByList.addAll(groupByList);
        where.orderByList.addAll(orderByList);
        where.havingGroup.andAll(havingGroup.conditions());
        where.limit = limit;
        where.offset = offset;
        return where;
    }

    @Override
    public String getQuery() {
        String fromQuery = whereBase.getQuery().trim();
        StringBuilder queryBuilder = new StringBuilder(fromQuery).append(" ");
        StringUtils.appendQualifier(queryBuilder,"WHERE", operatorGroup.getQuery());
        StringUtils.appendQualifier(queryBuilder,"GROUP BY", StringUtils.joinToString(groupByList,","));
        StringUtils.appendQualifier(queryBuilder,"HAVING", havingGroup.getQuery());
        StringUtils.appendQualifier(queryBuilder,"ORDER BY", StringUtils.joinToString(orderByList,","));

        if (limit > VALUE_UNSET) {
            StringUtils.appendQualifier(queryBuilder, "LIMIT", String.valueOf(limit));
        }
        if (offset > VALUE_UNSET) {
            StringUtils.appendQualifier(queryBuilder, "OFFSET", String.valueOf(offset));
        }

        return queryBuilder.toString();
    }

    /**
     * Joins the [SQLOperator] by the prefix of "AND" (unless its the first condition).
     * @param condition condition
     * @return this
     */
    public Where<T> and(SQLOperator condition) {
        operatorGroup.and(condition);
        return this;
    }

    /**
     * Joins the [SQLOperator] by the prefix of "OR" (unless its the first condition).
     * @param condition condition
     * @return this
     */
    public Where<T> or(SQLOperator condition) {
        operatorGroup.or(condition);
        return this;
    }

    /**
     * Joins all of the [SQLOperator] by the prefix of "AND" (unless its the first condition).
     * @param conditions conditions
     * @return this
     */
    public Where<T> andAll(List<SQLOperator> conditions) {
        operatorGroup.andAll(conditions);
        return this;
    }

    /**
     * Joins all of the [SQLOperator] by the prefix of "AND" (unless its the first condition).
     * @param conditions conditions
     * @return this
     */
    public Where<T> andAll(SQLOperator... conditions) {
        operatorGroup.andAll(conditions);
        return this;
    }

    @Override
    public Where<T> groupBy(NameAlias... nameAliases) {
        groupByList.addAll(Arrays.asList(nameAliases));
        return this;
    }

    @Override
    public Where<T> groupBy(IProperty<?>... properties) {
        for (IProperty<?> property : properties){
            groupByList.add(property.nameAlias());
        }
        return this;
    }

    /**
     * Defines a SQL HAVING statement without the HAVING.
     *
     * @param conditions The array of [SQLOperator]
     * @return this
     */
    @Override
    public Where<T> having(SQLOperator... conditions) {
        havingGroup.andAll(conditions);
        return this;
    }

    @Override
    public Where<T> orderBy(NameAlias nameAlias, boolean ascending) {
        orderByList.add(OrderBy.fromNameAlias(nameAlias, ascending));
        return this;
    }

    @Override
    public Where<T> orderBy(IProperty<?> property, boolean ascending) {
        orderByList.add(OrderBy.fromProperty(property, ascending));
        return this;
    }

    @Override
    public Where<T> orderBy(OrderBy orderBy) {
        orderByList.add(orderBy);
        return this;
    }

    /**
     * For use in ContentProvider generation. Appends all ORDER BY here.
     *
     * @param orderByList The order by.
     * @return this instance.
     */
    @Override
    public Where<T> orderByAll(List<OrderBy> orderByList) {
        this.orderByList.addAll(orderByList);
        return this;
    }

    @Override
    public Where<T> limit(long count) {
        this.limit = count;
        return this;
    }

    @Override
    public Where<T> offset(long offset) {
        this.offset = offset;
        return this;
    }

    /**
     * Specify that we use an EXISTS statement for this Where class.
     *
     * @param where The query to use in the EXISTS clause. Such as SELECT * FROM `MyTable` WHERE ... etc.
     * @return This where with an EXISTS clause.
     */
    public Where<T> exists(Where<?> where) {
        operatorGroup.and(new ExistenceOperator(where));
        return this;
    }

    /**
     * Queries for all of the results
     * @param databaseWrapper databaseWrapper
     * @return the result of the query as a [FlowCursor].
     */
    @Override
    public FlowCursor cursor(DatabaseWrapper databaseWrapper) {
        // Query the sql here
        if(whereBase.queryBuilderBase() instanceof Select){
            return databaseWrapper.rawQuery(getQuery(), null);
        }else {
            return super.cursor(databaseWrapper);
        }
    }

    /**
     * Queries for all of the results this statement returns from a DB cursor in the form of the [T]
     *
     * @param databaseWrapper databaseWrapper
     * @return All of the entries in the DB converted into [T]
     */
    @Override
    public List<T> queryList(DatabaseWrapper databaseWrapper) {
        checkSelect("cursor");
        return super.queryList(databaseWrapper);
    }

    /**
     * Queries and returns only the first [T] result from the DB. Will enforce a limit of 1 item
     * returned from the database.
     *
     * @param databaseWrapper databaseWrapper
     * @return The first result of this query. Note: this cursor forces a limit of 1 from the database.
     */
    @Override
    public T querySingle(DatabaseWrapper databaseWrapper) {
        checkSelect("cursor");
        limit(1);
        return super.querySingle(databaseWrapper);
    }

    @Override
    public Class<T> table() {
        return table;
    }

    private void checkSelect(String methodName) {
        if (!(whereBase.queryBuilderBase() instanceof Select)) {
            throw new IllegalArgumentException("Please use " + methodName + "(). The beginning is not a ISelect");
        }
    }
}
